package org.xx.armory.spring5.data;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import static java.util.Arrays.asList;
import static org.xx.armory.commons.StringConverter.join;
import static org.xx.armory.commons.Validators.rejectIfBlank;
import static org.xx.armory.commons.Validators.rejectIfNegative;
import static org.xx.armory.commons.Validators.rejectIfNull;

/**
 * 分页参数。
 */
public class PageArgs
        implements Pageable, Serializable {
    private static final long serialVersionUID = 1L;

    private final int startRowIndex;
    private final int maximumRows;
    private final List<Sort.Order> orders;

    /**
     * 构造分页参数对象。
     *
     * @param startRowIndex
     *         起始记录序号（从0开始）。
     * @param maximumRows
     *         最多记录数。{@code 0} 表示不分页。
     * @param orders
     *         排序规则。
     * @throws IllegalArgumentException
     *         如果参数 {@code startRowIndex} 小于 {@code 0}，或者参数 {@code maximumRows} 小于 {@code 0}。
     */
    protected PageArgs(
            int startRowIndex,
            int maximumRows,
            Sort.Order... orders
    ) {
        this.startRowIndex = rejectIfNegative(startRowIndex, "startRowIndex");
        this.maximumRows = rejectIfNegative(maximumRows, "maximumRows");

        final var ordersList = new ArrayList<Sort.Order>();
        if (orders != null) {
            ordersList.addAll(asList(orders));
        }
        this.orders = ordersList;
    }

    /**
     * 构造一个表示<strong>不分页</strong>的分页参数对象。
     *
     * @return <strong>不分页</strong>的分页参数对象。
     */
    public static PageArgs unpaged() {
        return of(0, 0);
    }

    /**
     * 构造分页参数对象。
     *
     * @param startRowIndex
     *         起始记录序号（从0开始）。
     * @param maximumRows
     *         最多记录数。{@code 0} 表示不分页。
     * @param orders
     *         排序规则。
     * @return 根据分页参数构造的分页参数对象。
     * @throws IllegalArgumentException
     *         如果参数 {@code startRowIndex} 小于 {@code 0}，或者参数 {@code maximumRows} 小于 {@code 0}。
     */
    public static PageArgs of(
            int startRowIndex,
            int maximumRows,
            Sort.Order... orders
    ) {
        return new PageArgs(startRowIndex, maximumRows, orders);
    }

    public final PageArgs asc(
            String property
    ) {
        return this.asc(property, Sort.NullHandling.NATIVE);
    }

    public final PageArgs desc(
            String property
    ) {
        return this.desc(property, Sort.NullHandling.NATIVE);
    }

    public final PageArgs asc(
            String property,
            Sort.NullHandling nullHandling
    ) {
        rejectIfBlank(property, "property");
        rejectIfNull(nullHandling, "nullHandling");

        return addOrder(Sort.Order.asc(property).with(nullHandling));
    }

    public final PageArgs desc(
            String property,
            Sort.NullHandling nullHandling
    ) {
        rejectIfBlank(property, "property");
        rejectIfNull(nullHandling, "nullHandling");

        return addOrder(Sort.Order.desc(property).with(nullHandling));
    }

    public final PageArgs addOrder(
            Sort.Order order
    ) {
        this.orders.add(order);
        return this;
    }

    public final Sort.Order[] getOrders() {
        return this.orders.toArray(new Sort.Order[0]);
    }

    /**
     * 获取起始记录数。
     *
     * @return 起始记录数。
     */
    public final int getStartRowIndex() {
        return startRowIndex;
    }

    /**
     * 获取最多记录数。
     *
     * @return 最多记录数。
     */
    public final int getMaximumRows() {
        return maximumRows;
    }

    @Override
    public boolean isPaged() {
        return this.maximumRows > 0;
    }

    @Override
    public int getPageNumber() {
        if (this.maximumRows <= 0) {
            throw new AssertionError();
        }
        return this.startRowIndex / this.maximumRows;
    }

    @Override
    public int getPageSize() {
        if (this.maximumRows <= 0) {
            throw new AssertionError();
        }
        return this.maximumRows;
    }

    @Override
    public long getOffset() {
        if (this.maximumRows < 0) {
            throw new AssertionError();
        }
        return this.startRowIndex;
    }

    @Override
    @NonNull
    public Sort getSort() {
        final Sort.Order[] orders = getOrders();
        return orders.length > 0 ? Sort.by(orders) : Sort.unsorted();
    }

    @Override
    @NonNull
    public Pageable next() {
        if (this.maximumRows <= 0) {
            return this;
        }
        return new PageArgs(this.startRowIndex + this.maximumRows, this.maximumRows, this.getOrders());
    }

    @Override
    @NonNull
    public Pageable previousOrFirst() {
        if (this.maximumRows <= 0) {
            return this;
        }
        int startRowIndex = this.startRowIndex - this.maximumRows;
        if (startRowIndex < 0) {
            startRowIndex = 0;
        }
        return new PageArgs(startRowIndex, this.maximumRows, this.getOrders());
    }

    @Override
    @NonNull
    public Pageable first() {
        if (this.maximumRows <= 0) {
            return this;
        }
        return new PageArgs(0, this.maximumRows, this.getOrders());
    }

    @Override
    @NonNull
    public Pageable withPage(
            int i
    ) {
        if (i < 0) {
            throw new AssertionError();
        }

        return new PageArgs(this.getPageSize() * i, this.maximumRows, this.getOrders());
    }

    @Override
    public boolean hasPrevious() {
        return this.maximumRows > 0 && this.startRowIndex > 0;
    }

    @Override
    public final int hashCode() {
        return Objects.hash(this.startRowIndex, this.maximumRows, Arrays.hashCode(this.getOrders()));
    }

    @Override
    public final boolean equals(
            Object o
    ) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        final PageArgs other = (PageArgs) o;
        return other.startRowIndex == this.startRowIndex && other.maximumRows == this.maximumRows &&
                Objects.equals(this.orders, other.orders);
    }

    @Override
    public final String toString() {
        var s = "(" + this.startRowIndex + "-" + (this.startRowIndex + this.maximumRows);
        if (!this.orders.isEmpty()) {
            s = s + ", [" + join(",", this.orders.stream()) + "]";
        }
        return s + ")";
    }
}
